package com.sixnimmt.app.server.game;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.atmosphere.cpr.MetaBroadcaster;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.sixnimmt.app.client.game.GameService;
import com.sixnimmt.app.server.game.domain.Game;
import com.sixnimmt.app.server.game.domain.Player;
import com.sixnimmt.app.server.game.domain.PlayerChoice;
import com.sixnimmt.app.shared.game.domain.Board;
import com.sixnimmt.app.shared.game.domain.Card;
import com.sixnimmt.app.shared.game.domain.PlayCardResult;
import com.sixnimmt.app.shared.game.exception.ActionUnauthorizedException;
import com.sixnimmt.app.shared.game.exception.ChoiceAlreadyMadeException;
import com.sixnimmt.app.shared.game.exception.GameAlreadyStartedException;
import com.sixnimmt.app.shared.game.exception.InvalidPlayerException;
import com.sixnimmt.app.shared.game.exception.InvalidStackException;
import com.sixnimmt.app.shared.game.exception.NameAlreadyUsedException;
import com.sixnimmt.app.shared.game.exception.NotInHandException;
import com.sixnimmt.app.shared.game.exception.ServerException;
import com.sixnimmt.app.shared.game.information.ServerInformation;
import com.sixnimmt.app.shared.game.notification.CardAddedNotification;
import com.sixnimmt.app.shared.game.notification.CardsTakenNotification;
import com.sixnimmt.app.shared.game.notification.GameNotification;
import com.sixnimmt.app.shared.game.notification.GameOverNotification;
import com.sixnimmt.app.shared.game.notification.HasChoosenCardNotification;
import com.sixnimmt.app.shared.game.notification.HaveToChooseCardNotification;
import com.sixnimmt.app.shared.game.notification.HaveToChooseStackNotification;
import com.sixnimmt.app.shared.game.notification.InitPlayerHandNotification;
import com.sixnimmt.app.shared.game.notification.PlayerAddedNotification;
import com.sixnimmt.app.shared.game.notification.RevealCardsNotification;
import com.sixnimmt.app.shared.game.notification.RoundOverNotification;
import com.sixnimmt.app.shared.game.notification.ServerRestartedNotification;
import com.sixnimmt.app.shared.game.notification.StackChangeNotification;

/**
 * The server side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class GameServiceImpl extends RemoteServiceServlet implements GameService {

	private static final Logger log = Logger.getLogger(GameServiceImpl.class.getName());

	public GameServiceImpl() {
		super();
		GameHolder.setGame(new Game());
	}

	public GameServiceImpl(Object delegate) {
		super(delegate);
		GameHolder.setGame(new Game());
	}

	@Override
	public void startGame(String playerName) throws InvalidPlayerException, GameAlreadyStartedException {

		log.info("Game started by  " + playerName);
		checkPlayerSession(playerName);

		if (GameHolder.getGame().isStarted()) {
			log.warning("Game already started");
			throw new GameAlreadyStartedException();
		}

		GameHolder.getGame().setStarted(true);

		startRound();
	}

	private void startRound() {
		log.info("New round");
		GameHolder.getGame().getDeck().init();
		for (int i = 0; i < Game.CARDS_BY_PLAYER; i++) {
			for (Player player : GameHolder.getGame().getPlayers().values()) {
				player.addCardInHand(GameHolder.getGame().getDeck().distribute());
			}
		}
		GameHolder
				.getGame()
				.getBoard()
				.init(new Card[] { GameHolder.getGame().getDeck().distribute(),
						GameHolder.getGame().getDeck().distribute(), GameHolder.getGame().getDeck().distribute(),
						GameHolder.getGame().getDeck().distribute() });

		StackChangeNotification[] stackInitNotifications = new StackChangeNotification[Board.STACK_NUMBER];
		for (int i = 0; i < Board.STACK_NUMBER; i++) {
			stackInitNotifications[i] = new StackChangeNotification(
					GameHolder.getGame().getBoard().getStacks()[i].getCards(), i);
		}

		for (Player player : GameHolder.getGame().getPlayers().values()) {
			List<Card> playerHand = new ArrayList<Card>();
			playerHand.addAll(player.getHand());
			broadcastNotifications(player.getName(), new InitPlayerHandNotification(playerHand));
		}
		broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, stackInitNotifications);
		GameHolder.getGame().setChoicesInProgress(true);
		broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, new HaveToChooseCardNotification());
	}

	@Override
	public ServerInformation retrieveServerInformation() {
		if (GameHolder.getGame().isStarted())
			return ServerInformation.GAME_ALREADY_STARTED;
		if (GameHolder.getGame().getPlayers().size() >= Game.MAX_PLAYERS)
			return ServerInformation.SERVER_FULL;

		return ServerInformation.OK;
	}

	@Override
	public List<String> joinGame(String playerName) throws NameAlreadyUsedException, ServerException {
		if (!retrieveServerInformation().equals(ServerInformation.OK)) {
			throw new ServerException();
		}

		for (Player player : GameHolder.getGame().getPlayers().values()) {
			if (playerName.equals(player.getName())) {
				log.info("Player " + playerName + " already in-game");
				throw new NameAlreadyUsedException();
			}
		}

		List<String> otherPlayers = new ArrayList<String>();
		for (Player otherPlayer : GameHolder.getGame().getPlayers().values()) {
			broadcastNotifications(otherPlayer.getName(), new PlayerAddedNotification(playerName));
			otherPlayers.add(otherPlayer.getName());
		}

		Player newPlayer = new Player(playerName, getThreadLocalRequest().getSession().getId());
		GameHolder.getGame().getPlayers().put(playerName, newPlayer);

		log.info("Player " + playerName + " with session id " + newPlayer.getSessionId() + "has joined");

		return otherPlayers;
	}

	private void broadcastNotifications(String playerName, GameNotification... notifications) {
		for (GameNotification gameNotification : notifications) {
			MetaBroadcaster.getDefault().broadcastTo(
					RetrieveGameNotificationsHandler.BROADCASTER_PREFIX_ID + playerName, gameNotification);
		}
	}

	@Override
	public void playCard(String playerName, Card card) throws NotInHandException, InvalidPlayerException,
			ChoiceAlreadyMadeException, ActionUnauthorizedException {
		checkPlayerSession(playerName);

		if (!GameHolder.getGame().isChoicesInProgress()) {
			throw new ActionUnauthorizedException();
		}

		if (GameHolder.getGame().isChoiceMade(playerName)) {
			throw new ChoiceAlreadyMadeException();
		}

		Player player = GameHolder.getGame().getPlayers().get(playerName);
		PlayerChoice playerChoice = player.playCard(card);
		GameHolder.getGame().getPlayerChoices().add(playerChoice);

		for (Player otherPlayer : GameHolder.getGame().getPlayers().values()) {
			if (!otherPlayer.equals(player)) {
				broadcastNotifications(otherPlayer.getName(), new HasChoosenCardNotification(player.getName()));
			}
		}

		if (GameHolder.getGame().getPlayerChoices().size() == GameHolder.getGame().getPlayers().size()) {
			GameHolder.getGame().setChoicesInProgress(false);

			Collections.sort(GameHolder.getGame().getPlayerChoices());

			revealCardToPlayers();

			Thread t = new Thread(new PerformPlayerChoicesRunnable());
			t.start();
		}

	}

	private void revealCardToPlayers() {
		Map<String, Card> choicesMap = new HashMap<String, Card>();
		for (PlayerChoice playerChoice : GameHolder.getGame().getPlayerChoices()) {
			choicesMap.put(playerChoice.getPlayer().getName(), playerChoice.getCard());
		}

		broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, new RevealCardsNotification(choicesMap));
	}

	@Override
	public void chooseStack(String playerName, int stack) throws InvalidStackException, InvalidPlayerException,
			ActionUnauthorizedException {
		checkPlayerSession(playerName);

		if (!GameHolder.getGame().isStackChoiceInProgress()) {
			throw new ActionUnauthorizedException();
		}

		if (stack < 0 || stack > 3) {
			throw new InvalidStackException();
		}

		PlayerChoice playerChoice = GameHolder.getGame().getPlayerChoices().get(0);
		if (playerName.equals(playerChoice.getPlayer())) {
			throw new InvalidPlayerException();
		}

		GameHolder.getGame().setStackChoiceInProgress(false);

		Thread t = new Thread(new PerformPlayerStackChoiceRunnable(stack));
		t.start();

	}

	private void checkPlayerSession(String playerName) throws InvalidPlayerException {
		Player p = GameHolder.getGame().getPlayers().get(playerName);
		if (p == null) {
			log.warning("Player " + playerName + " is not in game");
			throw new InvalidPlayerException();
		} else if (!p.getSessionId().equals(getThreadLocalRequest().getSession().getId())) {
			log.warning("Player " + playerName + " is in game but with different session id (" + p.getSessionId()
					+ " != " + getThreadLocalRequest().getSession().getId() + ")");
			throw new InvalidPlayerException();
		}
	}

	private void performPlayerStackChoice(int stack) {
		PlayerChoice playerChoice = GameHolder.getGame().getPlayerChoices().get(0);
		PlayCardResult result = GameHolder.getGame().getBoard().playCard(playerChoice.getCard(), stack);
		playerChoice.getPlayer().takeCards(result.getTakenCards());
		sendNotificationForCardPlayed(playerChoice, result);
		GameHolder.getGame().getPlayerChoices().remove(0);
	}

	private void performPlayerChoices() {
		while (!GameHolder.getGame().getPlayerChoices().isEmpty()) {
			PlayerChoice playerChoice = GameHolder.getGame().getPlayerChoices().get(0);

			if (playerChoice.getCard().getValue() < GameHolder.getGame().getBoard().getMinValueCardForAllStack()
					.getValue()) {

				GameHolder.getGame().setStackChoiceInProgress(true);
				broadcastNotifications(playerChoice.getPlayer().getName(), new HaveToChooseStackNotification());
				return;

			} else {

				PlayCardResult result = GameHolder.getGame().getBoard().playCard(playerChoice.getCard());
				playerChoice.getPlayer().takeCards(result.getTakenCards());
				sendNotificationForCardPlayed(playerChoice, result);

			}

			GameHolder.getGame().getPlayerChoices().remove(0);
		}

		Player aPlayer = GameHolder.getGame().getPlayers().values().iterator().next();
		if (aPlayer.getHand().isEmpty()) {
			Map<String, Integer> scores = new HashMap<String, Integer>();
			boolean gameIsOver = false;
			for (Player player : GameHolder.getGame().getPlayers().values()) {
				player.updateScore();
				player.clearCards();
				scores.put(player.getName(), player.getScore());
				if (player.getScore() >= Game.MAX_SCORE) {
					gameIsOver = true;
				}
			}
			RoundOverNotification roundOverNotification;
			if (gameIsOver) {
				roundOverNotification = new GameOverNotification(scores);
				log.info("Game over!");
			} else {
				roundOverNotification = new RoundOverNotification(scores);
				log.info("Round over!");
			}
			broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, roundOverNotification);

			if (!gameIsOver) {
				startRound();
			} else {
				log.info("Server restarted because game is over");
				restartServer();
			}
		} else {
			GameHolder.getGame().setChoicesInProgress(true);
			broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, new HaveToChooseCardNotification());
		}

	}

	private void restartServer() {
		GameHolder.setGame(new Game());
	}

	private void sendNotificationForCardPlayed(PlayerChoice playerChoice, PlayCardResult result) {

		// on attends entre chaque envoie 2 secondes.
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			log.warning(e.toString());
		}
		if (!result.getTakenCards().isEmpty()) {
			broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, new StackChangeNotification(
					Collections.<Card> emptyList(), result.getStackIndex()));
			broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, new CardsTakenNotification(
					playerChoice.getPlayer().getName(), playerChoice.getPlayer().getTakenCards().size()));
		}
		broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, new CardAddedNotification(playerChoice
				.getPlayer().getName(), playerChoice.getCard(), result.getStackIndex(), result.getPosition()));
	}

	@Override
	public void restartServer(String playerName) throws InvalidPlayerException {
		checkPlayerSession(playerName);

		broadcastNotifications(RetrieveGameNotificationsHandler.ALL_PLAYERS, new ServerRestartedNotification());

		log.info("Server restarted by user " + playerName);
		restartServer();
	}

	private class PerformPlayerChoicesRunnable implements Runnable {

		@Override
		public void run() {
			performPlayerChoices();
		}

	}

	private class PerformPlayerStackChoiceRunnable implements Runnable {

		private final int stack;

		public PerformPlayerStackChoiceRunnable(int stack) {
			this.stack = stack;
		}

		@Override
		public void run() {
			performPlayerStackChoice(stack);
			performPlayerChoices();
		}
	}
}
